using System;
using System.Net;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Configuration;
using System.Web.Services.Protocols;

using CyberSource.Clients;
using CyberSource.Clients.SoapWebReference;

namespace Commerce.Providers
{

/// <summary>
/// BETA VERSION PROVIDED AS-IS
/// 12/10/2007 - Paul Sterling
/// Check for updates to this provider here:
///         http://www.motusconnect.com/dashcommerce/providers.aspx
///         Send feedback to: dc@motusconnect.com
/// 
/// 
/// This CyberSourcePaymentProvider uses SOAP to submit requests
///  and retrieve responses from CyberSource
/// </summary>
public class CyberSourcePaymentProvider : PaymentProvider
{
	public CyberSourcePaymentProvider()
	{  }

    #region Passed-in Properties

    string      merchantID = "";
    string      keysDirectory = "";
    bool        sendToProduction = false;       // default set to false
    int         connectionLimit = -1;           // set to '-1' by default (2 connections)
    bool        authService = false;            // default set to false
    bool        captureService = false;         // default set to false
    string      currencyCode = "";           

    #endregion

    #region Provider specific behaviors

    public override void Initialize(string name, NameValueCollection config)
    {

        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException("config");

        // Assign the provider a default name if it doesn't have one
        if (String.IsNullOrEmpty(name))
        {
            config.Remove("name");
            config.Add("name",
                "CyberSourcePaymentProvider");
        }
        // Add a default "description" attribute to config if the
        //  attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description",
                "CyberSource Payment Provider");
        }
        base.Initialize(name, config);

        merchantID = config["merchantID"].ToString();
        if (String.IsNullOrEmpty(merchantID))
            throw new ProviderException("Empty CyberSource merchantID value");
        config.Remove("merchantID");

        keysDirectory = config["keysDirectory"].ToString();
        if (String.IsNullOrEmpty(keysDirectory))
            throw new ProviderException("Empty CyberSource keysDirectory value");
        config.Remove("keysDirectory");

        sendToProduction = bool.Parse(config["sendToProduction"]); 
        
        authService = bool.Parse(config["authService"]);
       
        captureService = bool.Parse(config["captureService"]);

        currencyCode = config["currencyCode"].ToString();
        if (String.IsNullOrEmpty(currencyCode))
            throw new ProviderException("Empty CyberSource currencyCode value");
        config.Remove("currencyCode");

    }

    public override string Name
    {
        get
        {
            return null;
        }
    }
    #endregion

    #region Provider Methods

    public override Commerce.Common.Transaction Charge(Commerce.Common.Order order)
    {
        decimal dTotal = order.OrderTotal;
        int roundTo = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalDigits;
        dTotal = Math.Round(dTotal, roundTo);

        // set up CyberSource config object
        //  use the values set in the provider section of web.config
        CyberSource.Clients.Configuration config = new CyberSource.Clients.Configuration();
        config.MerchantID = merchantID;
        config.KeysDirectory = keysDirectory;
        config.SendToProduction = sendToProduction;
        config.ConnectionLimit = connectionLimit;

        // TODO:  use this only for testing
        // config.Demo = true;

        // set up SOAP request
        RequestMessage request = new RequestMessage();

        // Credit Card Authorization - should always be true
        if (authService) 
        {
            request.ccAuthService = new CCAuthService();
            request.ccAuthService.run = "true";
        }

        // Credit Card Capture
        if (captureService)
        {
            request.ccCaptureService = new CCCaptureService();
            request.ccCaptureService.run = "true";
        }

        // add required fields
        request.merchantReferenceCode = order.OrderNumber;
        request.comments = order.OrderGUID;                     // set the comment to the order GUID

        BillTo billTo = new BillTo();
        billTo.firstName = order.BillingAddress.FirstName;
        billTo.lastName = order.BillingAddress.LastName;
        billTo.street1 = order.BillingAddress.Address1;
        billTo.city = order.BillingAddress.City;
        billTo.state = order.BillingAddress.StateOrRegion;
        billTo.postalCode = order.BillingAddress.Zip;
        billTo.country = order.BillingAddress.Country;
        billTo.email = order.Email;
        billTo.ipAddress = order.UserIP.ToString();
        request.billTo = billTo;

        Card card = new Card();
        card.accountNumber = order.CreditCardNumber;
        card.expirationMonth = order.CreditCardExpireMonth.ToString();
        card.expirationYear = order.CreditCardExpireYear.ToString();
        card.cvNumber = order.CreditCardSecurityNumber;
        request.card = card;

        PurchaseTotals purchaseTotals = new PurchaseTotals();
        purchaseTotals.currency = currencyCode;
        purchaseTotals.taxAmount = order.TaxAmount.ToString();
        purchaseTotals.freightAmount = order.ShippingAmount.ToString();
        purchaseTotals.grandTotalAmount = dTotal.ToString();
        request.purchaseTotals = purchaseTotals;

        // add detail items
        int itemCount = order.Items.Count;
        request.item = new Item[itemCount];

        for (int i = 0; i < itemCount; i++)
        {
            Item item = new Item();
            item.id = i.ToString();
            item.unitPrice = order.Items[i].PricePaid.ToString();
            item.totalAmount = order.Items[i].LineTotal.ToString();
            item.productName = order.Items[i].ProductName;
            item.quantity = order.Items[i].Quantity.ToString();

            request.item[i] = item;
        }

        // you can add optional fields here per your business needs

        // charge the card and get the return data to see what happened
        // I've kept the inspection to a minimum
        // this could be more robust if you have lots of failures
        Commerce.Common.Transaction trans = new Commerce.Common.Transaction();

        try
        {
            ReplyMessage reply = CyberSource.Clients.SoapClient.RunTransaction(config, request);

            if ("ACCEPT".Equals(reply.decision.ToUpper()))
            {
                trans.GatewayResponse = reply.decision;
                trans.TransactionNotes = ProcessReply(reply);
                trans.AuthorizationCode = reply.ccAuthReply.authorizationCode;
            }

            if ("REJECT".Equals(reply.decision.ToUpper()))
            {
                throw new Exception("Declined:  " + ProcessReply(reply));
            }

        }
        // some specific error handlers
        catch (SignException se)
        {
            throw new Exception("Gateway Error: " + HandleSignException(se));
        }
        catch (SoapHeaderException she)
        {
            throw new Exception("Gateway Error: " + HandleSoapHeaderException(she));
        }
        catch (SoapBodyException sbe)
        {
            throw new Exception("Gateway Error: " + HandleSoapBodyException(sbe));
        }
        catch (WebException we)
        {
            throw new Exception("Gateway Error: " + HandleWebException(we));
        }
        catch (Exception e)
        {
            throw new Exception("Gateway Error - undefined: " + e.Message);
        }

        // Return the tranasaction object for use by the Business Controller
        return trans;
    }

    public override string Refund(Commerce.Common.Order order)
    {
        throw new Exception("This method not enabled for CyberSource Payment Provider");
    }

    private static string ProcessReply(ReplyMessage reply)
    {
        string content = GetContent(reply);
        return content;
    }

    private static string GetContent(ReplyMessage reply)
    {

        int reasonCode = int.Parse(reply.reasonCode);
        switch (reasonCode)
        {
            // Success
            case 100:
                return (
                    "\nRequest ID: " + reply.requestID +
                    "\nAuthorization Code: " +
                        reply.ccAuthReply.authorizationCode +
                    "\nCapture Request Time: " +
                        reply.ccCaptureReply.requestDateTime +
                    "\nCaptured Amount: " +
                        reply.ccCaptureReply.amount);

            // Missing field(s)
            case 101:
                return (
                    "\nThe following required field(s) are missing: " +
                    EnumerateValues(reply.missingField));

            // Invalid field(s)
            case 102:
                return (
                    "\nThe following field(s) are invalid: " +
                    EnumerateValues(reply.invalidField));

            // Insufficient funds
            case 204:
                return (
                    "\nInsufficient funds in the account.  Please use a " +
                    "different card or select another form of payment.");

            // Bad account number
            case 231:
                return (
                    "\nInvalid account number.  Please check the account " +
                    "number.");

            // add additional reason codes here that you need to handle

            default:
                // For all other reason codes, return an empty string,
                return (String.Empty);
        }
    }

    private static string HandleSignException(SignException se)
    {
        string content = String.Format(
            "\nFailed to sign the request with error code '{0}' and " +
            "message '{1}'.", se.ErrorCode, se.Message);

        return content;
    }

    private static string HandleSoapHeaderException(
        SoapHeaderException she)
    {
        string content = String.Format(
            "\nA SOAP header exception was returned with fault code " +
            "'{0}' and message '{1}'.", she.Code, she.Message);

        return content;
    }

    private static string HandleSoapBodyException(SoapBodyException sbe)
    {
        string content = String.Format(
            "\nA SOAP body exception was returned with fault code " +
            "'{0}' and message '{1}'.", sbe.Code, sbe.Message);

        return content;
        }

    private static string HandleWebException(WebException we)
    {
        string content = String.Format(
            "\nFailed to get a response with status '{0}' and " +
            "message '{1}'", we.Status, we.Message);

        if (IsCriticalError(we))
        {
            // log this event as the payment may have been
            //  successfully processed
        }

        return content;

    }

    private static string EnumerateValues(string[] array)
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        foreach (string val in array)
        {
            sb.Append(val + "\n");
        }

        return (sb.ToString());
    }

    private static bool IsCriticalError(WebException we)
    {
        switch (we.Status)
        {
            case WebExceptionStatus.ProtocolError:
                if (we.Response != null)
                {
                    HttpWebResponse response
                        = (HttpWebResponse)we.Response;

                    // GatewayTimeout may be returned if you are
                    // connecting through a proxy server.
                    return (response.StatusCode ==
                        HttpStatusCode.GatewayTimeout);

                }

                // In case of ProtocolError, the Response property
                // should always be present.  In the unlikely case 
                // that it is not, we assume something went wrong
                // along the way and to be safe, treat it as a
                // critical error.
                return (true);

            case WebExceptionStatus.ConnectFailure:
            case WebExceptionStatus.NameResolutionFailure:
            case WebExceptionStatus.ProxyNameResolutionFailure:
            case WebExceptionStatus.SendFailure:
                return (false);

            default:
                return (true);
        }
    }

    #endregion
}
}
